const fs = require("fs");
const path = require("path");
const server = require("./server");

module.exports.getManifest = function (fullScan, emuPath, checkFiles) {
  var baseUrl = "http://swgaow.duckdns.org:60000/";
  var files = [];

  if (
    fullScan ||
    (emuPath && !fs.existsSync(path.join(emuPath, "swgemu_machineoptions.iff")))
  ) {
    files.push({
      name: "swgemu_machineoptions.iff",
      size: 0,
      md5: "",
      url: baseUrl + "swgemu_machineoptions.iff",
    });
  }
  var http = require(server.manifestUrl.indexOf("https") === 0 ? "https" : "http");
  var getOpts = server.manifestUrl.indexOf("https") === 0 ? { rejectUnauthorized: false } : {};
  try {
  http.get(server.manifestUrl, getOpts, function (res) {
    if (res.statusCode != 200) {
      if (module.exports.error) module.exports.error("Server error: HTTP " + res.statusCode);
      return;
    }
    var data = [];
    res.on("data", function (chunk) { data.push(chunk); });
    res.on("end", function () {
      var buf = Buffer.concat(data);
      var str = buf.toString("utf8");
      var start = str.indexOf("{");
      if (start > 0) str = str.substr(start);
      try {
        var parsed = JSON.parse(str);
      } catch (e) {
        if (module.exports.error) module.exports.error("Invalid JSON: " + e.message);
        return;
      }
      var list = parsed.files;
      if (!list || !list.length) {
        if (module.exports.error) module.exports.error("No files in manifest");
        return;
      }
      for (var i = 0; i < list.length; i++) {
        var f = list[i];
        files.push({
          name: f.name,
          size: f.size,
          md5: "",
          url: baseUrl + f.name,
        });
      }
      if (checkFiles) checkFiles(files);
    });
  }).on("error", function (err) {
    if (module.exports.error) module.exports.error("Connection error: " + err.message);
  });
  } catch(ex) {
    if (module.exports.error) module.exports.error("Exception: " + ex.message);
  }
};

function unionByName(a, b) {
  var lookup = {};
  for (var i of b) lookup[i.name] = true;
  var r = [];
  for (var i of a) if (!lookup[i.name]) r.push(i);
  for (var i of b) r.push(i);
  return r;
}

var forks = [];
var canceling = true;
module.exports.install = function (swgPath, emuPath, fullScan) {
  const child_process = require("child_process");
  canceling = false;
  module.exports.getManifest(fullScan, emuPath, checkFiles);

  var fileIndex = 0;
  var completedBytes = 0;
  var totalBytes = 0;
  var files;
  function checkFiles(manifest) {
    files = manifest;
    for (let file of files) {
      totalBytes += file.size;
      if (/\.zip$/.test(file.name)) totalBytes += file.size;
    }

    var env = { swgPath, emuPath };
    if (fullScan) env.fullScan = true;
    for (let i = 0; i < 4; i++) {
      let fork = child_process.fork(__filename, { env });
      fork.on("message", (m) => installedCallback(fork, m));
      fork.send(files[fileIndex++]);
      forks.push(fork);
    }
  }
  function installedCallback(fork, message) {
    if (typeof message == "string") {
      if (message.indexOf("err:") == 0) {
        if (module.exports.error) module.exports.error(message);
        if (fileIndex == files.length) {
          forks.splice(forks.indexOf(fork), 1);
          fork.kill();
        } else fork.send(files[fileIndex++]);
      }
      return;
    }
    if (message.currentFile !== undefined) {
      if (module.exports.currentFile) module.exports.currentFile(message.currentFile);
    } else if (message.complete !== undefined) {
      completedBytes += message.complete;
      progress(completedBytes, totalBytes);
      if (fileIndex == files.length) {
        forks.splice(forks.indexOf(fork), 1);
        fork.kill();
      } else fork.send(files[fileIndex++]);
    } else if (message.progress) {
      completedBytes += message.progress;
      progress(completedBytes, totalBytes);
    }
  }
  function progress(completed, total) {
    if (module.exports.progress && !canceling)
      module.exports.progress(completed, total);
  }
};

module.exports.cancel = function () {
  canceling = true;
  for (var fork of forks) fork.kill();
  forks = [];
};

if (process.send) {
  const AdmZip = require("adm-zip");

  function mkdirp(pathToCreate) {
    pathToCreate.split(path.sep).reduce((currentPath, folder) => {
      currentPath += folder + path.sep;
      if (!fs.existsSync(currentPath)) fs.mkdirSync(currentPath);
      return currentPath;
    }, "");
  }
  mkdirp(process.env.emuPath);

  var cancelDownload = null;
  process.on("message", (fileInfo) => {
    if (cancelDownload) cancelDownload();
    process.send({ currentFile: fileInfo.name });
    let src = path.join(process.env.swgPath, fileInfo.name);
    let dst = path.join(process.env.emuPath, fileInfo.name);
    let progressReported = 0;
    let canceled = false;
    cancelDownload = function () { canceled = true; };
    mkdirp(path.dirname(dst));
    if (fs.existsSync(dst)) src = dst;
    fs.stat(src, (err, stats) => {
      if (canceled) return;
      if (err) {
        if (err.code === "ENOENT") return doDownload();
        process.send("err: " + err);
        return;
      }
      if (stats.size != fileInfo.size) {
        process.send(
          "size mismatch actual: " + stats.size + " expected: " + fileInfo.size
        );
        return doDownload();
      }
      if (process.env.fullScan)
        md5(
          src,
          (hash) => {
            if (canceled) return;
            if (hash != fileInfo.md5) {
              process.send(
                "md5 mismatch actual: " + hash + " expected: " + fileInfo.md5
              );
              progress(-fileInfo.size);
              return doDownload();
            }
            process.send("md5 matches " + fileInfo.name);
            copyFile(src, dst, complete, progress);
          },
          progress
        );
      else copyFile(src, dst, complete, progress);
    });
    function doDownload() {
      console.log("Downloading " + fileInfo.url + " to " + dst);
      process.send("downloading " + fileInfo.url + " to " + dst);
      download(fileInfo.url, dst, complete, progress);
    }
    function complete(err) {
      if (canceled) return;
      if (err) return process.send("err:" + err);
      var expectedSize = fileInfo.size;
      if (/\.zip$/.test(fileInfo.name)) expectedSize *= 2;
      process.send({ complete: expectedSize - progressReported });
    }
    function progress(bytes) {
      if (canceled) return;
      progressReported += bytes;
      process.send({ progress: bytes });
    }
  });

  function download(url, dest, complete, progress) {
    try {
      fs.unlinkSync(dest);
    } catch (ex) {}
    var file = fs.createWriteStream(dest);
    var mod = require(url.indexOf("https") === 0 ? "https" : "http");
    var getOpts = url.indexOf("https") === 0 ? { rejectUnauthorized: false } : {};
    var finished = false;
    function finish(err) {
      if (finished) return;
      finished = true;
      file.close();
      if (err) {
        fs.unlink(dest, function () {
          if (complete) complete(err.message || err);
        });
      } else {
        if (/\.zip$/.test(dest)) unzip(dest, complete);
        else complete();
      }
    }
    var req = mod.get(url, getOpts, function (res) {
      res.on("data", function (d) {
        progress(d.length);
      });
      res.on("end", finish);
      res.on("error", finish);
      res.pipe(file);
    });
    req.on("error", finish);
  }

  function unzip(dest, complete) {
    process.send("unzipping");
    var zip = new AdmZip(dest);
    zip.extractAllToAsync(process.env.emuPath, true, complete);
  }

  function md5(file, complete, progress) {
    var hash = require("crypto").createHash("md5");
    var stream = fs.createReadStream(file);
    stream.on("data", (data) => {
      progress(data.length);
      hash.update(data, "utf8");
    });
    stream.on("end", () => complete(hash.digest("hex")));
  }

  function copyFile(source, target, complete, progress) {
    if (source == target) return complete();
    var cbCalled = false;
    var rd = fs.createReadStream(source);
    rd.on("error", done);
    var wr = fs.createWriteStream(target);
    wr.on("error", done);
    wr.on("close", done);
    rd.on("data", (d) => progress(d.length));
    rd.pipe(wr);
    function done(err) {
      if (!cbCalled) {
        complete(err);
        cbCalled = true;
      }
    }
  }
}
